/* eslint-disable */
// [4]   	NameStartChar	   ::=   	":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
// [4a]   	NameChar	   ::=   	NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
// [5]   	Name	   ::=   	NameStartChar (NameChar)*
const nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/// \u10000-\uEFFFF
const nameChar = new RegExp('[\\-\\.0-9' + nameStartChar.source.slice(1, -1) + '\\u00B7\\u0300-\\u036F\\u203F-\\u2040]')
const tagNamePattern = new RegExp('^' + nameStartChar.source + nameChar.source + '*(?:\:' + nameStartChar.source + nameChar.source + '*)?$')
// var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/
// var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',')

// S_TAG,	S_ATTR,	S_EQ,	S_ATTR_NOQUOT_VALUE
// S_ATTR_SPACE,	S_ATTR_END,	S_TAG_SPACE, S_TAG_CLOSE
const S_TAG = 0// tag name offerring
const S_ATTR = 1// attr name offerring
const S_ATTR_SPACE = 2// attr name end and space offer
const S_EQ = 3//= space?
const S_ATTR_NOQUOT_VALUE = 4// attr value(no quot value only)
const S_ATTR_END = 5// attr value end and no space(quot end)
const S_TAG_SPACE = 6// (attr value end || tag end ) && (space offer)
const S_TAG_CLOSE = 7// closed el<el />

function XMLReader() {

}

XMLReader.prototype = {
  parse(source, defaultNSMap, entityMap) {
    const domBuilder = this.domBuilder
    domBuilder.startDocument()
    _copy(defaultNSMap, defaultNSMap = {})
    parse(source, defaultNSMap, entityMap,
      domBuilder, this.errorHandler)
    domBuilder.endDocument()
  }
}
function parse(source, defaultNSMapCopy, entityMap, domBuilder, errorHandler) {
  function fixedFromCharCode(code) {
    // String.prototype.fromCharCode does not supports
    // > 2 bytes unicode chars directly
    if (code > 0xffff) {
      code -= 0x10000
      const surrogate1 = 0xd800 + (code >> 10)
				 const surrogate2 = 0xdc00 + (code & 0x3ff)

      return String.fromCharCode(surrogate1, surrogate2)
    } else {
      return String.fromCharCode(code)
    }
  }
  function entityReplacer(a) {
    const k = a.slice(1, -1)
    if (k in entityMap) {
      return entityMap[k]
    } else if (k.charAt(0) === '#') {
      return fixedFromCharCode(parseInt(k.substr(1).replace('x', '0x')))
    } else {
      errorHandler.error('entity not found:' + a)
      return a
    }
  }
  function appendText(end) { // has some bugs
    if (end > start) {
      const xt = source.substring(start, end).replace(/&#?\w+;/g, entityReplacer)
      locator && position(start)
      domBuilder.characters(xt, 0, end - start)
      start = end
    }
  }
  function position(p, m) {
    while (p >= lineEnd && (m = linePattern.exec(source))) {
      lineStart = m.index
      lineEnd = lineStart + m[0].length
      locator.lineNumber++
      // console.log('line++:',locator,startPos,endPos)
    }
    locator.columnNumber = p - lineStart + 1
  }
  var lineStart = 0
  var lineEnd = 0
  var linePattern = /.*(?:\r\n?|\n)|.*$/g
  var locator = domBuilder.locator

  const parseStack = [{currentNSMap: defaultNSMapCopy}]
  const closeMap = {}
  var start = 0
  while (true) {
    try {
      var tagStart = source.indexOf('<', start)
      if (tagStart < 0) {
        if (!source.substr(start).match(/^\s*$/)) {
          const doc = domBuilder.doc
	    			const text = doc.createTextNode(source.substr(start))
	    			doc.appendChild(text)
	    			domBuilder.currentElement = text
        }
        return
      }
      if (tagStart > start) {
        appendText(tagStart)
      }
      switch (source.charAt(tagStart + 1)) {
        case '/':
          var end = source.indexOf('>', tagStart + 3)
          var tagName = source.substring(tagStart + 2, end)
          var config = parseStack.pop()
          if (end < 0) {
	        		tagName = source.substring(tagStart + 2).replace(/[\s<].*/, '')
	        		// console.error('#@@@@@@'+tagName)
	        		errorHandler.error('end tag name: ' + tagName + ' is not complete:' + config.tagName)
	        		end = tagStart + 1 + tagName.length
	        	} else if (tagName.match(/\s</)) {
	        		tagName = tagName.replace(/[\s<].*/, '')
	        		errorHandler.error('end tag name: ' + tagName + ' maybe not complete')
	        		end = tagStart + 1 + tagName.length
          }
          // console.error(parseStack.length,parseStack)
          // console.error(config);
          var localNSMap = config.localNSMap
          var endMatch = config.tagName == tagName
          var endIgnoreCaseMach = endMatch || config.tagName && config.tagName.toLowerCase() == tagName.toLowerCase()
		        if (endIgnoreCaseMach) {
		        	domBuilder.endElement(config.uri, config.localName, tagName)
            if (localNSMap) {
              for (const prefix in localNSMap) {
                domBuilder.endPrefixMapping(prefix)
              }
            }
            if (!endMatch) {
		            	errorHandler.fatalError('end tag name: ' + tagName + ' is not match the current start tagName:' + config.tagName)
            }
		        } else {
		        	parseStack.push(config)
		        }

          end++
          break
          // end elment
        case '?':// <?...?>
          locator && position(tagStart)
          end = parseInstruction(source, tagStart, domBuilder)
          break
        case '!':// <!doctype,<![CDATA,<!--
          locator && position(tagStart)
          end = parseDCC(source, tagStart, domBuilder, errorHandler)
          break
        default:
          locator && position(tagStart)
          var el = new ElementAttributes()
          var currentNSMap = parseStack[parseStack.length - 1].currentNSMap
          // elStartEnd
          var end = parseElementStartPart(source, tagStart, el, currentNSMap, entityReplacer, errorHandler)
          var len = el.length


          if (!el.closed && fixSelfClosed(source, end, el.tagName, closeMap)) {
            el.closed = true
            if (!entityMap.nbsp) {
              errorHandler.warning('unclosed xml attribute')
            }
          }
          if (locator && len) {
            const locator2 = copyLocator(locator, {})
            // try{//attribute position fixed
            for (let i = 0; i < len; i++) {
              const a = el[i]
              position(a.offset)
              a.locator = copyLocator(locator, {})
            }
            // }catch(e){console.error('@@@@@'+e)}
            domBuilder.locator = locator2
            if (appendElement(el, domBuilder, currentNSMap)) {
              parseStack.push(el)
            }
            domBuilder.locator = locator
          } else if (appendElement(el, domBuilder, currentNSMap)) {
            parseStack.push(el)
          }


          if (el.uri === 'http://www.w3.org/1999/xhtml' && !el.closed) {
            end = parseHtmlSpecialContent(source, end, el.tagName, entityReplacer, domBuilder)
          } else {
            end++
          }
      }
    } catch (e) {
      errorHandler.error('element parse error: ' + e)
      // errorHandler.error('element parse error: '+e);
      end = -1
      // throw e;
    }
    if (end > start) {
      start = end
    } else {
      // TODO: 这里有可能sax回退，有位置错误风险
      appendText(Math.max(tagStart, start) + 1)
    }
  }
}
function copyLocator(f, t) {
  t.lineNumber = f.lineNumber
  t.columnNumber = f.columnNumber
  return t
}

/**
 * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack);
 * @return end of the elementStartPart(end of elementEndPart for selfClosed el)
 */
function parseElementStartPart(source, start, el, currentNSMap, entityReplacer, errorHandler) {
  let attrName
  var value
  let p = ++start
  let s = S_TAG// status
  while (true) {
    let c = source.charAt(p)
    switch (c) {
      case '=':
        if (s === S_ATTR) { // attrName
          attrName = source.slice(start, p)
          s = S_EQ
        } else if (s === S_ATTR_SPACE) {
          s = S_EQ
        } else {
          // fatalError: equal must after attrName or space after attrName
          throw new Error('attribute equal must after attrName')
        }
        break
      case '\'':
      case '"':
        if (s === S_EQ || s === S_ATTR // || s == S_ATTR_SPACE
        ) { // equal
          if (s === S_ATTR) {
            errorHandler.warning('attribute value must after "="')
            attrName = source.slice(start, p)
          }
          start = p + 1
          p = source.indexOf(c, start)
          if (p > 0) {
            value = source.slice(start, p).replace(/&#?\w+;/g, entityReplacer)
            el.add(attrName, value, start - 1)
            s = S_ATTR_END
          } else {
            // fatalError: no end quot match
            throw new Error('attribute value no end \'' + c + '\' match')
          }
        } else if (s == S_ATTR_NOQUOT_VALUE) {
          value = source.slice(start, p).replace(/&#?\w+;/g, entityReplacer)
          // console.log(attrName,value,start,p)
          el.add(attrName, value, start)
          // console.dir(el)
          errorHandler.warning('attribute "' + attrName + '" missed start quot(' + c + ')!!')
          start = p + 1
          s = S_ATTR_END
        } else {
          // fatalError: no equal before
          throw new Error('attribute value must after "="')
        }
        break
      case '/':
        switch (s) {
          case S_TAG:
            el.setTagName(source.slice(start, p))
          case S_ATTR_END:
          case S_TAG_SPACE:
          case S_TAG_CLOSE:
            s = S_TAG_CLOSE
            el.closed = true
          case S_ATTR_NOQUOT_VALUE:
          case S_ATTR:
          case S_ATTR_SPACE:
            break
            // case S_EQ:
          default:
            throw new Error("attribute invalid close char('/')")
        }
        break
      case '':// end document
        // throw new Error('unexpected end of input')
        errorHandler.error('unexpected end of input')
        if (s == S_TAG) {
          el.setTagName(source.slice(start, p))
        }
        return p
      case '>':
        switch (s) {
          case S_TAG:
            el.setTagName(source.slice(start, p))
          case S_ATTR_END:
          case S_TAG_SPACE:
          case S_TAG_CLOSE:
            break// normal
          case S_ATTR_NOQUOT_VALUE:// Compatible state
          case S_ATTR:
            value = source.slice(start, p)
            if (value.slice(-1) === '/') {
              el.closed = true
              value = value.slice(0, -1)
            }
          case S_ATTR_SPACE:
            if (s === S_ATTR_SPACE) {
              value = attrName
            }
            if (s == S_ATTR_NOQUOT_VALUE) {
              errorHandler.warning('attribute "' + value + '" missed quot(")!!')
              el.add(attrName, value.replace(/&#?\w+;/g, entityReplacer), start)
            } else {
              if (currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)) {
                errorHandler.warning('attribute "' + value + '" missed value!! "' + value + '" instead!!')
              }
              el.add(value, value, start)
            }
            break
          case S_EQ:
            throw new Error('attribute value missed!!')
        }
        //			console.log(tagName,tagNamePattern,tagNamePattern.test(tagName))
        return p
        /* xml space '\x20' | #x9 | #xD | #xA; */
      case '\u0080':
        c = ' '
      default:
        if (c <= ' ') { // space
          switch (s) {
            case S_TAG:
              el.setTagName(source.slice(start, p))// tagName
              s = S_TAG_SPACE
              break
            case S_ATTR:
              attrName = source.slice(start, p)
              s = S_ATTR_SPACE
              break
            case S_ATTR_NOQUOT_VALUE:
              var value = source.slice(start, p).replace(/&#?\w+;/g, entityReplacer)
              errorHandler.warning('attribute "' + value + '" missed quot(")!!')
              el.add(attrName, value, start)
            case S_ATTR_END:
              s = S_TAG_SPACE
              break
				// case S_TAG_SPACE:
				// case S_EQ:
				// case S_ATTR_SPACE:
				//	void();break;
				// case S_TAG_CLOSE:
					// ignore warning
          }
        } else { // not space
          // S_TAG,	S_ATTR,	S_EQ,	S_ATTR_NOQUOT_VALUE
          // S_ATTR_SPACE,	S_ATTR_END,	S_TAG_SPACE, S_TAG_CLOSE
          switch (s) {
            // case S_TAG:void();break;
            // case S_ATTR:void();break;
            // case S_ATTR_NOQUOT_VALUE:void();break;
            case S_ATTR_SPACE:
              var tagName = el.tagName
              if (currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)) {
                errorHandler.warning('attribute "' + attrName + '" missed value!! "' + attrName + '" instead2!!')
              }
              el.add(attrName, attrName, start)
              start = p
              s = S_ATTR
              break
            case S_ATTR_END:
              errorHandler.warning('attribute space is required"' + attrName + '"!!')
            case S_TAG_SPACE:
              s = S_ATTR
              start = p
              break
            case S_EQ:
              s = S_ATTR_NOQUOT_VALUE
              start = p
              break
            case S_TAG_CLOSE:
              throw new Error("elements closed character '/' and '>' must be connected to")
          }
        }
    }// end outer switch
    // console.log('p++',p)
    p++
  }
}
/**
 * @return true if has new namespace define
 */
function appendElement(el, domBuilder, currentNSMap) {
  const tagName = el.tagName
  let localNSMap = null
  // var currentNSMap = parseStack[parseStack.length-1].currentNSMap;
  var i = el.length
  while (i--) {
    var a = el[i]
    const qName = a.qName
    const value = a.value
    var nsp = qName.indexOf(':')
    if (nsp > 0) {
      var prefix = a.prefix = qName.slice(0, nsp)
      var localName = qName.slice(nsp + 1)
      var nsPrefix = prefix === 'xmlns' && localName
    } else {
      localName = qName
      prefix = null
      nsPrefix = qName === 'xmlns' && ''
    }
    // can not set prefix,because prefix !== ''
    a.localName = localName
    // prefix == null for no ns prefix attribute
    if (nsPrefix !== false) { // hack!!
      if (localNSMap == null) {
        localNSMap = {}
        // console.log(currentNSMap,0)
        _copy(currentNSMap, currentNSMap = {})
        // console.log(currentNSMap,1)
      }
      currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value
      a.uri = 'http://www.w3.org/2000/xmlns/'
      domBuilder.startPrefixMapping(nsPrefix, value)
    }
  }
  var i = el.length
  while (i--) {
    a = el[i]
    var prefix = a.prefix
    if (prefix) { // no prefix attribute has no namespace
      if (prefix === 'xml') {
        a.uri = 'http://www.w3.org/XML/1998/namespace'
      } if (prefix !== 'xmlns') {
        a.uri = currentNSMap[prefix || '']

        // {console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)}
      }
    }
  }
  var nsp = tagName.indexOf(':')
  if (nsp > 0) {
    prefix = el.prefix = tagName.slice(0, nsp)
    localName = el.localName = tagName.slice(nsp + 1)
  } else {
    prefix = null// important!!
    localName = el.localName = tagName
  }
  // no prefix element has default namespace
  const ns = el.uri = currentNSMap[prefix || '']
  domBuilder.startElement(ns, localName, tagName, el)
  // endPrefixMapping and startPrefixMapping have not any help for dom builder
  // localNSMap = null
  if (el.closed) {
    domBuilder.endElement(ns, localName, tagName)
    if (localNSMap) {
      for (prefix in localNSMap) {
        domBuilder.endPrefixMapping(prefix)
      }
    }
  } else {
    el.currentNSMap = currentNSMap
    el.localNSMap = localNSMap
    // parseStack.push(el);
    return true
  }
}
function parseHtmlSpecialContent(source, elStartEnd, tagName, entityReplacer, domBuilder) {
  if (/^(?:script|textarea)$/i.test(tagName)) {
    const elEndStart = source.indexOf('</' + tagName + '>', elStartEnd)
    let text = source.substring(elStartEnd + 1, elEndStart)
    if (/[&<]/.test(text)) {
      if (/^script$/i.test(tagName)) {
        // if(!/\]\]>/.test(text)){
        // lexHandler.startCDATA();
        domBuilder.characters(text, 0, text.length)
        // lexHandler.endCDATA();
        return elEndStart
        // }
      }// }else{//text area
      text = text.replace(/&#?\w+;/g, entityReplacer)
      domBuilder.characters(text, 0, text.length)
      return elEndStart
      // }
    }
  }
  return elStartEnd + 1
}
function fixSelfClosed(source, elStartEnd, tagName, closeMap) {
  // if(tagName in closeMap){
  let pos = closeMap[tagName]
  if (pos == null) {
    // console.log(tagName)
    pos = source.lastIndexOf('</' + tagName + '>')
    if (pos < elStartEnd) { // 忘记闭合
      pos = source.lastIndexOf('</' + tagName)
    }
    closeMap[tagName] = pos
  }
  return pos < elStartEnd
  // }
}
function _copy(source, target) {
  for (const n in source) { target[n] = source[n] }
}
function parseDCC(source, start, domBuilder, errorHandler) { // sure start with '<!'
  const next = source.charAt(start + 2)
  switch (next) {
    case '-':
      if (source.charAt(start + 3) === '-') {
        var end = source.indexOf('-->', start + 4)
        // append comment source.substring(4,end)//<!--
        if (end > start) {
          domBuilder.comment(source, start + 4, end - start - 4)
          return end + 3
        } else {
          errorHandler.error('Unclosed comment')
          return -1
        }
      } else {
        // error
        return -1
      }
    default:
      if (source.substr(start + 3, 6) == 'CDATA[') {
        var end = source.indexOf(']]>', start + 9)
        domBuilder.startCDATA()
        domBuilder.characters(source, start + 9, end - start - 9)
        domBuilder.endCDATA()
        return end + 3
      }
      // <!DOCTYPE
      // startDTD(java.lang.String name, java.lang.String publicId, java.lang.String systemId)
      var matchs = split(source, start)
      var len = matchs.length
      if (len > 1 && /!doctype/i.test(matchs[0][0])) {
        const name = matchs[1][0]
        const pubid = len > 3 && /^public$/i.test(matchs[2][0]) && matchs[3][0]
        const sysid = len > 4 && matchs[4][0]
        const lastMatch = matchs[len - 1]
        domBuilder.startDTD(name, pubid && pubid.replace(/^(['"])(.*?)\1$/, '$2'),
          sysid && sysid.replace(/^(['"])(.*?)\1$/, '$2'))
        domBuilder.endDTD()

        return lastMatch.index + lastMatch[0].length
      }
  }
  return -1
}


function parseInstruction(source, start, domBuilder) {
  const end = source.indexOf('?>', start)
  if (end) {
    const match = source.substring(start, end).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/)
    if (match) {
      const len = match[0].length
      domBuilder.processingInstruction(match[1], match[2])
      return end + 2
    } else { // error
      return -1
    }
  }
  return -1
}

/**
 * @param source
 */
function ElementAttributes(source) {

}
ElementAttributes.prototype = {
  setTagName(tagName) {
    if (!tagNamePattern.test(tagName)) {
      throw new Error('invalid tagName:' + tagName)
    }
    this.tagName = tagName
  },
  add(qName, value, offset) {
    if (!tagNamePattern.test(qName)) {
      throw new Error('invalid attribute:' + qName)
    }
    this[this.length++] = {qName, value, offset}
  },
  length: 0,
  getLocalName(i) { return this[i].localName },
  getLocator(i) { return this[i].locator },
  getQName(i) { return this[i].qName },
  getURI(i) { return this[i].uri },
  getValue(i) { return this[i].value }
//	,getIndex:function(uri, localName)){
//		if(localName){
//
//		}else{
//			var qName = uri
//		}
//	},
//	getValue:function(){return this.getValue(this.getIndex.apply(this,arguments))},
//	getType:function(uri,localName){}
//	getType:function(i){},
}


function split(source, start) {
  let match
  const buf = []
  const reg = /'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g
  reg.lastIndex = start
  reg.exec(source)// skip <
  while (match = reg.exec(source)) {
    buf.push(match)
    if (match[1]) return buf
  }
}

exports.XMLReader = XMLReader
